/*
 * Copyright 2010 david varnes.
 *
 * Licensed under the Apache License, version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at:
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.freeswitch.esl.client.transport.message;

import com.google.common.primitives.Bytes;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ReplayingDecoder;
import io.netty.handler.codec.TooLongFrameException;
import org.freeswitch.esl.client.transport.HeaderParser;
import org.freeswitch.esl.client.transport.message.EslHeaders.Name;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;

/**
 * Decoder used by the IO processing pipeline. Client consumers should never need to use
 * this class.
 * <p/>
 * Follows the following decode algorithm (from FreeSWITCH wiki)
 * <pre>
 *    Look for \n\n in your receive file
 *
 *    Examine data for existence of Content-Length
 *
 *    If NOT present, process event and remove from receive file
 *
 *    IF present, Shift file to remove 'header'
 *    Evaluate content-length value
 *
 *    Loop until receive file size is >= Content-length
 *    Extract content-length bytes from file and process
 * </pre>
 */
public class EslFrameDecoder extends ReplayingDecoder<EslFrameDecoder.State> {
	/**
	 * Line feed character
	 */
	static final byte LF = 10;

	protected enum State {
		READ_HEADER,
		READ_BODY,
	}

	private final Logger log = LoggerFactory.getLogger(this.getClass());
	private final int maxHeaderSize;
	private EslMessage currentMessage;
	private boolean treatUnknownHeadersAsBody = false;

	public EslFrameDecoder(int maxHeaderSize) {
		super(State.READ_HEADER);
		if (maxHeaderSize <= 0) {
			throw new IllegalArgumentException(
				"maxHeaderSize must be a positive integer: " +
					maxHeaderSize);
		}
		this.maxHeaderSize = maxHeaderSize;
	}

	public EslFrameDecoder(int maxHeaderSize, boolean treatUnknownHeadersAsBody) {
		this(maxHeaderSize);
		this.treatUnknownHeadersAsBody = treatUnknownHeadersAsBody;
	}

	@Override
	protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out) throws Exception {
		State state = state();

		log.trace("decode() : state [{}]", state);
		switch (state) {
			case READ_HEADER:
				if (currentMessage == null) {
					currentMessage = new EslMessage();
				}
				/*
								*  read '\n' terminated lines until reach a single '\n'
								*/
				boolean reachedDoubleLF = false;
				while (!reachedDoubleLF) {
					// this will read or fail
					String headerLine = readToLineFeedOrFail(buffer, maxHeaderSize);
					log.debug("read header line [{}]", headerLine);
					if (!headerLine.isEmpty()) {
						// split the header line
						String[] headerParts = HeaderParser.splitHeader(headerLine);
						Name headerName = Name.fromLiteral(headerParts[0]);
						if (headerName == null) {
							if (treatUnknownHeadersAsBody) {
								// cache this 'header' as a body line <-- useful for Outbound client mode
								currentMessage.addBodyLine(headerLine);
							} else {
								throw new IllegalStateException("Unhandled ESL header [" + headerParts[0] + ']');
							}
						}
						currentMessage.addHeader(headerName, headerParts[1]);
					} else {
						reachedDoubleLF = true;
					}
					// do not read in this line again
					checkpoint();
				}
				// have read all headers - check for content-length
				if (currentMessage.hasContentLength()) {
					checkpoint(State.READ_BODY);
					log.debug("have content-length, decoding body ..");
					//  force the next section

					break;
				} else {
					// end of message
					checkpoint(State.READ_HEADER);
					// send message upstream
					EslMessage decodedMessage = currentMessage;
					currentMessage = null;

					out.add(decodedMessage);
					break;
				}

			case READ_BODY:
				/*
								*   read the content-length specified
								*/
				int contentLength = currentMessage.getContentLength();
				ByteBuf bodyBytes = buffer.readBytes(contentLength);
				log.debug("read [{}] body bytes", bodyBytes.writerIndex());
				// most bodies are line based, so split on LF
				while (bodyBytes.isReadable()) {
					String bodyLine = readLine(bodyBytes, contentLength);
					log.debug("read body line [{}]", bodyLine);
					currentMessage.addBodyLine(bodyLine);
				}

				// end of message
				checkpoint(State.READ_HEADER);
				// send message upstream
				EslMessage decodedMessage = currentMessage;
				currentMessage = null;
				out.add(decodedMessage);
				try {
					if (bodyBytes.refCnt() == 1) {
						bodyBytes.release();
						bodyBytes = null;
					}
				} catch (Exception ex) {
					ex.printStackTrace();
				}
				break;

			default:
				throw new Error("Illegal state: [" + state + ']');
		}
	}

	private String readToLineFeedOrFail(ByteBuf buffer, int maxLineLegth) throws TooLongFrameException {
//		StringBuilder sb = new StringBuilder(64);
		List<Byte> byteList = new ArrayList<>();
		while (true) {
			// this read might fail
			byte nextByte = buffer.readByte();
			if (nextByte == LF) {
//				return sb.toString();
				return new String(Bytes.toArray(byteList));
			} else {

				// Abort decoding if the decoded line is too large.
//				if (sb.length() >= maxLineLegth) {
				if (byteList.size() >= maxLineLegth){
					throw new TooLongFrameException(
						"ESL header line is longer than " + maxLineLegth + " bytes.");
				}
//				sb.append((char) nextByte);
			   byteList.add(nextByte);
			}
		}
	}

	private String readLine(ByteBuf buffer, int maxLineLength) throws TooLongFrameException {
//		StringBuilder sb = new StringBuilder(64);
		List<Byte> byteList = new ArrayList<>();
		while (buffer.isReadable()) {
			// this read should always succeed
			byte nextByte = buffer.readByte();
			if (nextByte == LF) {
//				return sb.toString();
				return new String(Bytes.toArray(byteList));
			} else {
				// Abort decoding if the decoded line is too large.
//				if (sb.length() >= maxLineLength) {
				if (byteList.size() >= maxLineLength) {
					throw new TooLongFrameException(
						"ESL message line is longer than " + maxLineLength + " bytes.");
				}
//				sb.append((char) nextByte);
				byteList.add(nextByte);
			}
		}

//		return sb.toString();
		return new String(Bytes.toArray(byteList));
	}
}
